Repository navigation
feat(trace): local storage mode for AI coding sessions - #3617
matiasinsaurralde wants to merge 7 commits into
Conversation
AI Session Checks — 🟢 93% · ✅ 0 failing
|
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | ai-config-ai-agents-allowed |
ai-coding-session-7492b7 |
- |
| ✅ Passed | ai-config-no-dangerous-commands |
ai-coding-session-7492b7 |
- |
| ✅ Passed | ai-config-no-secrets |
ai-coding-session-7492b7 |
- |
| ✅ Passed | ai-config-mcp-servers-allowed |
ai-coding-session-7492b7 |
- |
Security Checks — evaluating
Policies are still being evaluated. Results will update once available.
Scans not applied (3)
| Scan | Reason |
|---|---|
vulnerability-scan |
no manifest/lockfile changed |
github-actions-scan |
no workflow files changed |
iac-scan |
no IaC files changed |
Security context
This change touches code with 6 recorded security-fix advisories. These are pointers to what past fixes established, not findings in this diff, and they never fail the check.
View in Chainloop ↗ · How this works ↗
Powered by Chainloop and Chainloop Trace
…ontrol plane Chainloop Trace records an AI coding session and, on git push, uploads a signed attestation to a control plane. This adds a local mode where a repository records its sessions and stores the assembled evidence set on disk, with no control plane, login, or network. The storage mode is a committed repository policy in .chainloop.yml: traceStorage (CONTROL_PLANE default, or LOCAL), an optional tracePath, and traceRequired (replacing the deprecated requireTrace). `trace init --local` sets a repository up with no control plane, and the pre-push hook then runs the same push-time assembly and writes each material under its content digest, with a per-session manifest that declares the output unsigned and not an attestation. `trace run` honors the mode as well. The stored evidence equals what a push would upload apart from the signature, and secret redaction stays on by default with a CHAINLOOP_TRACE_NO_REDACT opt-out for a trusted local store. In local mode no control-plane connection is opened: the trace commands build it lazily and only on the attestation path. The stored sessions are kept across a trace uninstall. Refs #3582 Chainloop-Trace-Sessions: 7492b77a-1dc6-4352-823f-f87c42672452 Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
7be3fc4 to
49b0b6c
Compare
The local store sized the merged annotations map from len(crafted)+len(extra), which a static analyzer flags as an allocation size that may overflow. Both maps hold only a handful of annotation keys, so the sum cannot overflow in practice; size the hint from one length and let the map grow instead. Refs #3582 Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
There was a problem hiding this comment.
All reported issues were addressed across 23 files
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
The local-mode session-start banner now says the session is recorded in local mode, so a developer can tell at a glance which storage mode is active without inferring it from the destination line (R-012). Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
A config read or parse error no longer selects CONTROL_PLANE silently, so a LOCAL repository with a broken .chainloop.yml cannot have its pre-push hook upload a local-only session. Local evidence is finalized only when every gathered session is stored, so a partial failure no longer strands a session's retry state. A control-plane trace run is reported in telemetry again, while a local run stays excluded. A trace run that stores its evidence surfaces a store failure when the wrapped command succeeded. Uninstall preserves a configured tracePath that resolves inside the trace directory. The local session-start banner states the storage timing that matches how the session is driven. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
…clone guidance A control-plane trace init over a repository previously set to LOCAL now writes traceStorage: CONTROL_PLANE, so the pre-push hook stops storing on disk and the init no longer disagrees with how the repository behaves. The local-mode next steps now tell each teammate to run chainloop trace init --local after cloning, since the managed git hooks live under .git and are not cloned, so a commit alone does not set a clone up to record. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
A control-plane trace init now surfaces an invalid traceStorage in an existing .chainloop.yml before installing hooks, instead of discarding the error and leaving a repository whose every pre-push fails on the same config. An explicit --local run still overrides the bad value. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
| if err != nil { | ||
| return err | ||
| } | ||
| cfg.local = localFlag |
There was a problem hiding this comment.
So the idea is that once the repository is configured as LOCAL, every session for every user will be local? What's the path to CONTROL_PLANE? do they need to run trace init again (and commit)? It would be good to have it documented.
There was a problem hiding this comment.
Yeah. Documented in app/cli/internal/trace/README.md under "Switching storage modes." Fuller treatment (a spec requirement for the lifecycle, and possibly an explicit --control-plane flag to mirror --local) I'd do as a separate follow-up.
|
|
||
| // diskMaterialSink implements specMaterialAdder by crafting each material | ||
| // offline and writing its content under its content digest. It is the local | ||
| // counterpart of AttestationExecutor: it runs the identical crafting — same |
There was a problem hiding this comment.
It'd be nice if both AttestationExecutor and DiskMaterialSink implement the same TraceStorage interface, not only to simplify the logic (removing if-elses), but also opening the option to future storage alternatives.
There was a problem hiding this comment.
At the material level there's already a common seam: specMaterialAdder (AddMaterial(ctx, name, path, kind, annotations) (digest, error)). Both backends implement it:
AttestationExecutor.AddMaterial(trace_attestation.go:219) → uploads to CAS.diskMaterialSink.AddMaterial(trace_local_store.go:240) → writes under the digest on disk.
The entire assembly (attachSessionEvidence,attachSkills, redaction, digesting) runs against that interface, so it's identical for both and is why a local store reproduces a push byte-for-byte.gatherPushSessionsand finalizePush are shared too. So the duplication is already mostly gone; what remains is two thin call-site branches (trace_hook.go:143 and trace_run.go:200) and the two flow functionsRunTracePush/RunTraceLocalStore.
The real obstacle to a naive TraceStorage is that the two flows differ in granularity, which is what makes a careless interface leak:
- Control plane: one attestation envelope for the whole push:
executor.Init→attachSessionEvidence(executor, allSessions)→ oneexecutor.Push. - Local: per-session, a loop, a fresh
diskMaterialSinkand amanifest.jsonper session.
Plus they differ in: connection setup (CP needs a lazily-built executor/identity; local needs nothing), the terminal step (signed in-toto envelope vs unsigned manifest), and error semantics (attest only on a clean run; local stores even an interrupted session). A per-session interface method (FinalizeSession) would not map onto the single-envelope CP model.
…ent it A control-plane trace init that clears a previous LOCAL policy now tells the user the repository switched and to commit .chainloop.yml, instead of changing the mode with only a debug log. The trace README documents how to switch storage modes in both directions and that each clone installs its own hooks. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
I'd NOT make it imply local since in the future we might allow more than one mode I'd also like to discuss the right default location, would it be or should it be |
Maybe here we can add, if in the future you wna to connect this repo to a chainloop control plane, run trace init again |
migmartri
left a comment
There was a problem hiding this comment.
@matiasinsaurralde ptal at my comments, I can't make it work as well as I added some additional feedback



Refs PFM-7675
Summary
Chainloop Trace records an AI coding session and attests it to a control plane on
git push. This adds a local storage mode: a repository records its sessions and stores the assembled evidence set on disk, with no control plane, login, or network.The mode is a committed repository policy in
.chainloop.yml:traceStorage-CONTROL_PLANE(default) orLOCALtracePath- optional location for stored sessions (defaults under the per-clone trace state)traceRequired- replaces the deprecatedrequireTracechainloop trace init --localsets a repository up with no control plane (R-008). In local mode the pre-push hook runs the same push-time assembly and writes each material under its content digest, with a per-session manifest that declares the output unsigned and not an attestation (R-002, R-003, R-006, R-009).trace runhonors the mode as well. Secret redaction stays on by default, with aCHAINLOOP_TRACE_NO_REDACTopt-out for a trusted local store (R-004). No control-plane connection is opened in local mode (R-005), and stored sessions are preserved across atrace uninstall.Implements the design in
docs/specs/issue-3582-trace-evidence-export.md.Refs #3582
Flow
trace init --local: